package com.nulldev.util.JVM.reflections;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.nulldev.util.JVM.reflections.cache.CachedReflection;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.data.Variables;

public class Reflections {

	public static boolean is(final String fullClassPath) {
		return ClassCache.classExists(fullClassPath);
	}

	public static Class<?> get(final String fullClassPath) throws ClassNotFoundException {
		return ClassCache.forName(fullClassPath);
	}

	public boolean EXPERIMENT_USE_CACHED_REFLECTIONS = false;

	private final Class<?> clazz;
	private final Object o;

	private Reflections(final Object classPath) {
		try {
			this.clazz = classPath.getClass();
			this.o = classPath;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public Reflections(final String classPath) {
		this(classPath, true);
	}

	public Reflections(final String classPath, final boolean initInstance) {
		try {
			this.clazz = ClassCache.forName(classPath);
			if (initInstance) {
				this.o = clazz.newInstance();
			} else {
				this.o = null;
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public Reflections(final String classPath, final Class<?>[] paramTypes, final Object... vars) {
		try {
			this.clazz = ClassCache.forName(classPath);
			this.o = this.clazz.getConstructor(paramTypes).newInstance(vars);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings(
		{ "all" })
	public Object staticInvoke(final String function) {
		if (this.EXPERIMENT_USE_CACHED_REFLECTIONS) {
			return CachedReflection.invoke(this.clazz, null, function);
		} else {
			try {
				return this.clazz.getMethod(function).invoke(null, null);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	@SuppressWarnings(
		{ "all" })
	public Object invoke(final String function) {
		if (this.EXPERIMENT_USE_CACHED_REFLECTIONS) {
			return CachedReflection.invoke(this.clazz, this.o, function);
		} else {
			try {
				return this.clazz.getMethod(function).invoke(this.o, null);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	public Object invoke(final String function, final Class<?>[] paramTypes, final Object... vars) {
		if (this.EXPERIMENT_USE_CACHED_REFLECTIONS) {
			return CachedReflection.invokeTo(null, this.clazz, this.o, function, vars, paramTypes);
		} else {
			try {
				return this.clazz.getMethod(function, paramTypes).invoke(this.o, vars);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	public Object invoke(final String function, final Object... vars) {
		if (this.EXPERIMENT_USE_CACHED_REFLECTIONS) {
			return CachedReflection.invoke(this.clazz, this.o, function, vars);
		} else {
			try {
				return this.clazz.getMethod(function).invoke(this.o, vars);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	public Object invokeDecl(final String function, final Object... vars) {
		if (this.EXPERIMENT_USE_CACHED_REFLECTIONS) {
			return CachedReflection.invoke(this.clazz, this.o, function, vars);
		} else {
			try {
				final Method method = this.o.getClass().getDeclaredMethod(function);
				if (!method.isAccessible())
					method.setAccessible(true);
				return method.invoke(this.o, vars);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	public Object invokeDecl(final String function, final Class<?>[] args, final Object... vars) {
		if (this.EXPERIMENT_USE_CACHED_REFLECTIONS) {
			return CachedReflection.invokeTo(null, this.clazz, this.o, function, vars, args);
		} else {
			try {
				return this.o.getClass().getDeclaredMethod(function, args).invoke(this.o, vars);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	public static Reflections use(final String string) {
		if (string == null)
			throw new NullPointerException("string == null!");
		if (string.isEmpty())
			throw new NullPointerException("string is empty!");
		return new Reflections(string, false);
	}

	public static Reflections encapsulate(final Object object) {
		if (object == null)
			throw new NullPointerException("object == null!");
		if (object instanceof Reflections) {
			return new Reflections(((Reflections) object).o);
		}
		return new Reflections(object);
	}

	public static Reflections encapsulate_experimental(final Object object) {
		if (object == null)
			throw new NullPointerException("object == null!");
		if (object instanceof Reflections) {
			return new Reflections(((Reflections) object).o).enableExperiment(true);
		}
		return new Reflections(object).enableExperiment(true);
	}

	public Reflections enableExperiment(final boolean state) {
		this.EXPERIMENT_USE_CACHED_REFLECTIONS = state;
		return this;
	}

	public Class<?> getClazz() {
		return this.clazz;
	}

	@Override
	public String toString() {
		return "Reflections[" + this.o + "]";
	}

	public Object var(final String string) {
		if (this.EXPERIMENT_USE_CACHED_REFLECTIONS) {
			return CachedReflection.getField(this.clazz, this.o, string);
		} else {
			try {
				return o.getClass().getField(string);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	public Object declVar(final String string) {
		if (this.EXPERIMENT_USE_CACHED_REFLECTIONS) {
			return CachedReflection.getField(this.clazz, null, string);
		} else {
			try {
				final Field f = this.clazz.getDeclaredField(string);
				if (f == null) {
					throw new NoSuchFieldException("declVar(String) -> " + string + " == null!");
				}
				if (!f.isAccessible()) {
					f.setAccessible(true);
				}
				return f.get(this.o);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	public Object declVar(final String string, final Object o) {
		try {
			final Field f = this.clazz.getDeclaredField(string);
			if (f == null) {
				throw new NoSuchFieldException("declVar(String, Object) -> " + string + " == null!");
			}
			if (!f.isAccessible()) {
				f.setAccessible(true);
			}
			return f.get(o);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings(
		{ "all" })
	public Method method(final String string) {
		try {
			return this.clazz.getMethod(string, null);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static Object make(final String cls) {
		try {
			return ClassCache.forName(cls).newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static Reflections reflect(final String string, final Object... items) throws Exception {
		if (string == null || string.isEmpty())
			throw new IllegalArgumentException("string cannot be null or empty!");
		final Class<?> c = ClassCache.forName(string);
		if (c == null)
			return null;
		Constructor<?> e = null;
		final String[] parameters = new String[items.length];
		if (parameters.length > 0) {
			for (int i = 0; i < parameters.length; i++) {
				String sn = items[i].getClass().getSimpleName();
				if (sn.equals("Integer"))
					sn = "int";
				else if (sn.equals("Long"))
					sn = "long";
				else if (sn.equals("Double"))
					sn = "double";
				else if (sn.equals("Float"))
					sn = "float";
				else if (sn.equals("Short"))
					sn = "short";
				parameters[i] = sn;
			}
			for (final Constructor<?> cs : c.getConstructors()) {
				int match = 0;
				for (final Parameter p : cs.getParameters()) {
					final String f = p.toString();
					final String name = f.substring(0, f.indexOf(" " + p.getName()));
					if (ArrayUtils.contains(parameters, name)) {
						match++;
						if (match >= parameters.length && cs.getParameters().length == parameters.length) {
							e = cs;
							break;
						}
					}
				}
			}
		}
		if (e == null)
			throw new RuntimeException("Failed to find proper constructor!");
		return new Reflections(e.newInstance(items));
	}

	public Object object() {
		return this.o;
	}

	public Set<String> methods() {
		final Set<String> out = new HashSet<String>();
		CachedReflection.methodNames(out, false, this.clazz);
		return out;
	}

	public Set<String> staticMethods() {
		final Set<String> out = new HashSet<String>();
		CachedReflection.methodNames(out, true, this.clazz);
		return out;
	}

	private static void removeFinalModifier(final Field field) {
		removeFinalModifier(field, true);
	}

	private static void removeFinalModifier(final Field field, final boolean forceAccess) {
		Variables.checkNotNull(field, "field");
		try {
			if (Modifier.isFinal(field.getModifiers())) {
				final Field modifiersField = Field.class.getDeclaredField("modifiers");
				final boolean doForceAccess = forceAccess && !modifiersField.isAccessible();
				if (doForceAccess) {
					modifiersField.setAccessible(true);
				}
				try {
					modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
				} finally {
					if (doForceAccess) {
						modifiersField.setAccessible(false);
					}
				}
			}
		} catch (final NoSuchFieldException | IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}

	public void assignVar(final String name, final Object object) {
		try {
			final Field f = this.clazz.getDeclaredField(name);
			if (!f.isAccessible())
				f.setAccessible(true);
			removeFinalModifier(f);
			f.set(null, object);
		} catch (Exception ex) {
			throw new RuntimeException(ex);
		}
	}

	/**
	 * Method that will find all sub-classes and implemented interfaces of a given
	 * class or interface. Classes are listed in order of precedence, starting with
	 * the immediate super-class, followed by interfaces class directly declares to
	 * implemented, and then recursively followed by parent of super-class and so
	 * forth. Note that <code>Object.class</code> is not included in the list
	 * regardless of whether <code>endBefore</code> argument is defined or not.
	 *
	 * @param endBefore Super-type to NOT include in results, if any; when
	 *                  encountered, will be ignored (and no super types are
	 *                  checked).
	 */
	public static List<Class<?>> findSuperTypes(Class<?> cls, Class<?> endBefore) {
		return findSuperTypes(cls, endBefore, new ArrayList<Class<?>>());
	}

	public static List<Class<?>> findSuperTypes(Class<?> cls, Class<?> endBefore, List<Class<?>> result) {
		_addSuperTypes(cls, endBefore, result, false);
		return result;
	}

	private static void _addSuperTypes(Class<?> cls, Class<?> endBefore, Collection<Class<?>> result, boolean addClassItself) {
		if (cls == endBefore || cls == null || cls == Object.class) {
			return;
		}
		if (addClassItself) {
			if (result.contains(cls)) { // already added, no need to check supers
				return;
			}
			result.add(cls);
		}
		for (Class<?> intCls : cls.getInterfaces()) {
			_addSuperTypes(intCls, endBefore, result, true);
		}
		_addSuperTypes(cls.getSuperclass(), endBefore, result, true);
	}
}
